Skip to content

AnchoredOverlay: Add Popover API, modifications to CSS anchor positioning#7677

Open
TylerJDev wants to merge 25 commits intomainfrom
tylerjdev/add-popover-support-css-anchor-positioning
Open

AnchoredOverlay: Add Popover API, modifications to CSS anchor positioning#7677
TylerJDev wants to merge 25 commits intomainfrom
tylerjdev/add-popover-support-css-anchor-positioning

Conversation

@TylerJDev
Copy link
Member

@TylerJDev TylerJDev commented Mar 18, 2026

Prerequisite to https://github.com/github/primer/issues/6448, part of https://github.com/github/primer/issues/6431

Utilizes the Popover API to power our CSS anchor positioning in AnchoredOverlay. Both the popover support and CSS anchor positioning are behind the primer_react_css_anchor_positioning feature flag.

Changelog

New

  • Adds popover support to AnchoredOverlay

Changed

  • Small modifications, bug fixes towards CSS anchor positioning

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Mar 18, 2026

🦋 Changeset detected

Latest commit: d7af05c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions bot added staff Author is a staff member integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm labels Mar 18, 2026
@github-actions
Copy link
Contributor

⚠️ Action required

👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Check the integration testing docs for step-by-step instructions. Or, apply the integration-tests: skipped manually label to skip these checks.

@primer
Copy link
Contributor

primer bot commented Mar 19, 2026

🤖 Lint issues have been automatically fixed and committed to this PR.

@TylerJDev TylerJDev added the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Mar 24, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7677 March 24, 2026 15:14 Inactive
@github-actions github-actions bot removed the update snapshots 🤖 Command that updates VRT snapshots on the pull request label Mar 24, 2026
@github-actions github-actions bot temporarily deployed to storybook-preview-7677 March 24, 2026 15:31 Inactive
@TylerJDev TylerJDev requested a review from liuliu-dev March 24, 2026 20:13
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces Popover API support to AnchoredOverlay (behind the primer_react_css_anchor_positioning feature flag) and updates the CSS anchor positioning approach, including adding an opt-out for rendering overlays in a Portal.

Changes:

  • Add disablePortal support to Overlay to allow rendering without a Portal when desired.
  • Update AnchoredOverlay to integrate Popover API (popover="manual", showPopover()), and to set anchor/position anchor styles via JS.
  • Extend unit/e2e coverage and Storybook examples (including a “Multiple Overlays” scenario) and add a changeset for a minor release.

Reviewed changes

Copilot reviewed 8 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/react/src/Overlay/Overlay.tsx Adds disablePortal prop and uses it to bypass Portal rendering.
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx Adds Popover API wiring + JS-managed anchor-name / position-anchor setup.
packages/react/src/AnchoredOverlay/AnchoredOverlay.test.tsx Updates tests for new portal behavior and feature-flag-specific behavior.
packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css Removes wrapper/anchor classes; adjusts anchored overlay styles for popover behavior.
packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx Adds “Multiple Overlays” story to validate multiple instances.
packages/react/src/ActionMenu/ActionMenu.test.tsx Removes assertions tied to the removed .Anchor CSS class.
e2e/components/AnchoredOverlay.test.ts Adds multi-overlay screenshot coverage.
.playwright/snapshots/** Adds new snapshots for the “Multiple Overlays” story.
.changeset/soft-pianos-carry.md Declares a minor release for the new AnchoredOverlay behavior behind the flag.

renderAnchor={props => <Button {...props}>Anchor Button</Button>}
onPositionChange={onPositionChange}
className={className}
{...overlayProps}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnchoredOverlayTestComponent is spreading the overlayProps test setting directly onto <AnchoredOverlay> ({...overlayProps}), which passes disablePortal as a top-level prop instead of as overlayProps={...}. This should be passed via the overlayProps prop (e.g. overlayProps={overlayProps}), otherwise TS should reject it and the test won't be exercising the intended behavior.

Suggested change
{...overlayProps}
overlayProps={overlayProps}

Copilot uses AI. Check for mistakes.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

@liuliu-dev liuliu-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! ✨

Just a few questions to make sure I understand it correctly.

onClick: onAnchorClick,
onKeyDown: onAnchorKeyDown,
...(cssAnchorPositioning ? {className: classes.Anchor} : {}),
...(cssAnchorPositioning ? {'data-anchor': '', popoverTarget: popoverId} : {}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find data-anchor used anywhere in the CSS, is this for future styles?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! We used to use it, but I removed it in a previous commit. I can go ahead and remove it!

Comment on lines +274 to +292
// Track the overlay element so we can re-run the effect when it changes.
// This is necessary when using a Portal, as React re-renders can replace
// the DOM node, causing the popover to lose its :popover-open state.
const overlayElement = overlayRef.current

useLayoutEffect(() => {
// Read ref inside effect to get the value after child refs are attached
const currentOverlay = overlayRef.current

if (!cssAnchorPositioning || !open || !currentOverlay) return
currentOverlay.style.setProperty('position-anchor', `--anchored-overlay-anchor-${id}`)
try {
if (!currentOverlay.matches(':popover-open')) {
currentOverlay.showPopover()
}
} catch {
// Ignore if popover is already showing or not supported
}
}, [cssAnchorPositioning, open, overlayElement, id, overlayRef])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this part. Is this because when a Portal remounts, it creates a new Overlay element and :popover-open is lost on this new element?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly! In steps this is how it works:

  • The user clicks the trigger button for AnchoredOverlay which turns open to true
  • We check if the overlay ref is not null (if it's null then the Overlay hasn't rendered yet)
  • We then check if it's open (along with other conditionals such as if the feature flag is enabled)
  • Lastly, we check if it's somehow open already, as we're using the popover api, we can do this by checking if :popover-open is present. This guards us from re-opening the overlay multiple times whenever one of the dependencies change.

Let me know if this makes sense, or if you have questions!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, and it's clearer after reading the conversation you and @siddharthkp had, thanks!

Copy link
Member

@siddharthkp siddharthkp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted some clarifying questions first

position-visibility: anchors-visible;
z-index: 100;
position: fixed;
position: fixed !important;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious, why do we need important here? Are there any overrides in dotcom that this might break?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a case where position was being overridden (example: https://github.com/github/github-ui/pull/16228). The !important will most likely be temporary, as we can probably make the selector take precedence in regards to specificity.


type ContainerProps = {
anchorSide?: AnchorSide
disablePortal?: boolean
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should make this prop part of the public API. Do we want folks to use it at an instance level? Should this be an invisible detail of using the feature flag?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's fair! This one is more so to test when/if we need the portal. Most of the cases where a portal came in handy was with styling. Any styling that the parent had would leak into the AnchoredOverlay, which the portal prevents.

I'll adjust this so that it's only useable with the flag along with making it "private".

}, [cssAnchorPositioning, anchorRef, overlayRef, id])

// Track the overlay element so we can re-run the effect when it changes.
// This is necessary when using a Portal, as React re-renders can replace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this comment because we seem to only want to use the ref when using css anchor positioning and popover, so not using a portal. Am I reading this wrong?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I'll adjust the comment 😅

This is trying to say that if the overlay is mounted/unmounted the popover itself is no longer open (via :popover-open). When it is mounted or re-rendered, we need to open it manually again via showPopover (if open is true) which we need the active ref for. Let me know if this makes sense or not!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhh okay. I understand this in theory, when does this happen in prod?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was running into an issue with Issues SelectPanel, where it would render the SelectPanel only after the initial click. It would be within the DOM but it wouldn't be opened as expected.

On first click, the component would mount and attach its ref. The useLayoutEffect runs with the new DOM node, checks that it's not already opened (via :popover-open), and calls showPopover() to actually display it. Without the effect, the element would exist in the DOM with but never be shown.

Since we're using popover='manual' we need to explicitly use showPopover() to display the popover. The effect ensures we call it whenever a new DOM node is created, whether from the initial open, or from dynamic/lazy mounting where the element gets recreated (such as Issues SelectPanel).

Let me know if I can go into more details too! I'll see about adding context on the Issues SelectPanel case which made me layer the effect this way in https://github.com/github/primer/issues/6321!

@primer-integration
Copy link

👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/17094

@primer-integration
Copy link

Integration test results from github/github-ui:

Passed  CI   Passed
Running  VRT   Running
Passed  Projects   Passed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm staff Author is a staff member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants